/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.impl.cmd;

import com.je.bpm.engine.ActivitiException;
import com.je.bpm.engine.ActivitiIllegalArgumentException;
import com.je.bpm.engine.ActivitiObjectNotFoundException;
import com.je.bpm.engine.impl.interceptor.Command;
import com.je.bpm.engine.impl.interceptor.CommandContext;
import com.je.bpm.engine.impl.persistence.deploy.DeploymentManager;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntity;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntityManager;
import com.je.bpm.engine.impl.persistence.entity.TaskEntity;
import com.je.bpm.engine.impl.util.ProcessDefinitionUtil;
import com.je.bpm.engine.repository.ProcessDefinition;
import com.je.bpm.engine.runtime.ProcessInstance;
import com.je.bpm.core.model.process.Process;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * {@link Command} that changes the process definition version of an existing process instance.
 * <p>
 * Warning: This command will NOT perform any migration magic and simply set the process definition version in the database, assuming that the user knows, what he or she is doing.
 * <p>
 * This is only useful for simple migrations. The new process definition MUST have the exact same activity id to make it still run.
 * <p>
 * Furthermore, activities referenced by sub-executions and jobs that belong to the process instance MUST exist in the new process definition version.
 * <p>
 * The command will fail, if there is already a {@link ProcessInstance} or {@link HistoricProcessInstance} using the new process definition version and the same business key as the
 * {@link ProcessInstance} that is to be migrated.
 * <p>
 * If the process instance is not currently waiting but actively running, then this would be a case for optimistic locking, meaning either the version update or the "real work" wins, i.e., this is a
 * race condition.
 *
 * @see http://forums.activiti.org/en/viewtopic.php?t=2918
 */
public class SetProcessDefinitionVersionCmd implements Command<Void>, Serializable {

    private static final long serialVersionUID = 1L;

    private final String processInstanceId;
    private final Integer processDefinitionVersion;

    public SetProcessDefinitionVersionCmd(String processInstanceId, Integer processDefinitionVersion) {
        if (processInstanceId == null || processInstanceId.length() < 1) {
            throw new ActivitiIllegalArgumentException("The process instance id is mandatory, but '" + processInstanceId + "' has been provided.");
        }
        if (processDefinitionVersion == null) {
            throw new ActivitiIllegalArgumentException("The process definition version is mandatory, but 'null' has been provided.");
        }
        if (processDefinitionVersion < 1) {
            throw new ActivitiIllegalArgumentException("The process definition version must be positive, but '" + processDefinitionVersion + "' has been provided.");
        }
        this.processInstanceId = processInstanceId;
        this.processDefinitionVersion = processDefinitionVersion;
    }

    public Void execute(CommandContext commandContext) {
        // check that the new process definition is just another version of the same
        // process definition that the process instance is using
        ExecutionEntityManager executionManager = commandContext.getExecutionEntityManager();
        ExecutionEntity processInstance = executionManager.findById(processInstanceId);
        if (processInstance == null) {
            throw new ActivitiObjectNotFoundException("No process instance found for id = '" + processInstanceId + "'.", ProcessInstance.class);
        } else if (!processInstance.isProcessInstanceType()) {
            throw new ActivitiIllegalArgumentException("A process instance id is required, but the provided id " + "'" + processInstanceId + "' " + "points to a child execution of process instance " + "'"
                    + processInstance.getProcessInstanceId() + "'. " + "Please invoke the " + getClass().getSimpleName() + " with a root execution id.");
        }

        DeploymentManager deploymentCache = commandContext.getProcessEngineConfiguration().getDeploymentManager();
        ProcessDefinition currentProcessDefinition = deploymentCache.findDeployedProcessDefinitionById(processInstance.getProcessDefinitionId());

        ProcessDefinition newProcessDefinition = deploymentCache
                .findDeployedProcessDefinitionByKeyAndVersionAndTenantId(currentProcessDefinition.getKey(), processDefinitionVersion, currentProcessDefinition.getTenantId());

        validateAndSwitchVersionOfExecution(commandContext, processInstance, newProcessDefinition);

        // switch the historic process instance to the new process definition version
        commandContext.getHistoryManager().recordProcessDefinitionChange(processInstanceId, newProcessDefinition.getId());

        // switch all sub-executions of the process instance to the new process definition version
        Collection<ExecutionEntity> childExecutions = executionManager.findChildExecutionsByProcessInstanceId(processInstanceId);
        for (ExecutionEntity executionEntity : childExecutions) {
            validateAndSwitchVersionOfExecution(commandContext, executionEntity, newProcessDefinition);
        }

        return null;
    }

    protected void validateAndSwitchVersionOfExecution(CommandContext commandContext, ExecutionEntity execution, ProcessDefinition newProcessDefinition) {
        // check that the new process definition version contains the current activity
        Process process = ProcessDefinitionUtil.getProcess(newProcessDefinition.getId());
        if (execution.getActivityId() != null && process.getFlowElement(execution.getActivityId(), true) == null) {
            throw new ActivitiException("The new process definition " + "(key = '" + newProcessDefinition.getKey() + "') " + "does not contain the current activity " + "(id = '"
                    + execution.getActivityId() + "') " + "of the process instance " + "(id = '" + processInstanceId + "').");
        }

        // switch the process instance to the new process definition version
        execution.setProcessDefinitionId(newProcessDefinition.getId());
        execution.setProcessDefinitionName(newProcessDefinition.getName());
        execution.setProcessDefinitionKey(newProcessDefinition.getKey());

        // and change possible existing tasks (as the process definition id is stored there too)
        List<TaskEntity> tasks = commandContext.getTaskEntityManager().findTasksByExecutionId(execution.getId());
        for (TaskEntity taskEntity : tasks) {
            taskEntity.setProcessDefinitionId(newProcessDefinition.getId());
            commandContext.getHistoryManager().recordTaskProcessDefinitionChange(taskEntity.getId(), newProcessDefinition.getId());
        }
    }

}
